-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reintroduce observable state,props,context in mobx-react #3863
base: main
Are you sure you want to change the base?
Conversation
🦋 Changeset detectedLatest commit: 3a8c951 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Hi @urugator what do you think about this PR? do you think it's safe to reintroduce the observability? |
While the global state version was the main motivation for this change, we had other reasons, some conceptual, some technical. I will share parts of our discussion:
Another problem with the implementation is that shallow equality check runs twice for props/state/context on every component update - in each setter to decide whether atom should If we are to reintroduce observable props (the decision is on @mweststrate), I would at least like to see the above problems addressed:
Both can be probably workarounded, but it's some extra work and unknown problems may emerge (getting back to pilling patches point). |
Hi @urugator thanks for the informative reply.
I tested your linked test case on my branch, it seems to me that Child does render 2 times in total which is desired as per comment, I'm not sure how to reproduce the unnecessary render (3 times) you described. In that example, Parent passes
I agree there are some performance benefits from achieving this, I spent few hours and I couldn't achieve it in a good manner. However, (this is hacking React) that react reconciler always call We would really appreciate it if we could make observable props behind a config, at least people like us can opt-in this feature |
hello @urugator I made some progress I made several changes to ObserverClass
however no.4 brings few tradeoffs
|
|
@@ -513,7 +509,7 @@ describe("should render component even if setState called with exactly the same | |||
act(() => clickableDiv.click()) | |||
act(() => clickableDiv.click()) | |||
|
|||
expect(renderCount).toBe(3) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
state is shallow compared now, renderCount should be 2
Hi @urugator @mweststrate I'm keen to hear you thoughts, |
1840137
to
6ff48f3
Compare
|
Thanks for the extensive work here! I'll need to study more carefully the effects of this change, but I'm quite swamped atm. Feel free to ping if I didn't circle back in ~2 weeks! |
unmount() | ||
}) | ||
|
||
test(`Observable props/state/context workaround`, () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Preserve this test.
@@ -1115,189 +1125,12 @@ test(`Observable changes in componenWillUnmount don't cause any warnings or erro | |||
consoleWarnSpy.mockRestore() | |||
}) | |||
|
|||
test(`Observable prop workaround`, () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Preserve this test.
container.querySelector("div")?.click() | ||
}) | ||
expect(container).toHaveTextContent("1") | ||
expect(renderCount).toBe(2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be 1. The actual number isn't super relevant, it can depend on some React batching implementation details.
The point is that we must not issue an update on ANY component, when a component is already updating.
We use isUpdating
flag to prevent updating self, but we must avoid calling forceUpdate
also on other components - in this case on Child component, which is subscribed to Parent.props
, because we leaked Parent.props
to Child
via callback.
It's like calling childRef.forceUpdate()
during Parent's render. We must not do that. It's an oversight in the original implementation. Once component is updating (Parent recieved new props) the only proper way to propagate change to Child is via props. This would normally happen if you would create the callback during render.
The test was supposed to demonstrate that while this behavior is not correct, there is this unintended "feature" that you can send the same callback ref and still get the update (which is not supposed to happen with memo
).
So to fix this, we probably just need to make isUpdating
flag global.
Another way to put it is, that props
must be observable for every derivation, except for observer
derivation - that is any user defined computed
/reaction
/autorun
.
Hope it's a bit clearer now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a bit less obvious example of usage that will break once it's fixed:
@observer
class Parent extends Component {
store = observable({
get cmp() {
return this.props.foo + 5;
}
})
render() {
return <Child store={store} />
}
}
@observer
class Child extends Component {
render() {
return store.cmp;
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@urugator I see what you mean now
yes this is an unintended "feature" that let child display "correct" value, therefore I feel removing it will cause unexpected issue in our fairly large codebase
that props must be observable for every derivation, except for observer derivation - that is any user defined computed/reaction/autorun.
This is something I can do, as per this commit, I can change globalState.trackingDerivation
to be an array, and add isReactObserver
(name TBD) to IDerivation
Eventually, during props' get method, I can check if other react observer is tracking it and show a error in console
As requested in #3858
This PR re-introduce the observable state, props, context. It is proven to be safe to restore this feature as the bug caused by setter is now gone.
Regarding two requests from @urugator
This does seem to happen according to exp.test.js
This is now solved by storing
shouldComponentUpdate
result in a WeakSet, so we don't re compare in setterNext: I solved the class component tearing by wrapping class component in a function component, then utilise
useSyncExternalStore
. I decided to keep it in separate PR after this is mergedCode change checklist
/docs
. For new functionality, at leastAPI.md
should be updatedyarn mobx test:performance
)